use crate::lexer;

use std::fmt;

pub enum Expr {
    Atom(f64),
    Cons(char, Vec<Expr>),
}

impl fmt::Display for Expr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Expr::Atom(i) => write!(f, "{}", i),
            Expr::Cons(head, rest) => {
                write!(f, "({}", head)?;
                for s in rest {
                    write!(f, " {}", s)?
                }
                write!(f, ")")
            }
        }
    }
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("unexpected token: {0:?}")]
    UnexpectedToken(lexer::Token),
    #[error("empty expression")]
    EmptyExpr,
    #[error("unclosed parenthesis")]
    UnclosedParen,
    #[error("{0}")]
    Lexer(#[from] lexer::Error),
}

impl Expr {
    pub fn new(input: &str) -> Result<Self, Error> {
        let mut lex = lexer::Lexer::new(input)?;
        Self::from_lexer(&mut lex, 0)
    }

    pub fn eval(&self) -> f64 {
        match self {
            Expr::Atom(i) => *i,
            Expr::Cons(op, operand) => match *op {
                '+' => match operand.len() {
                    1 => operand[0].eval(),
                    2 => operand[0].eval() + operand[1].eval(),
                    _ => unreachable!(),
                },
                '-' => match operand.len() {
                    1 => -operand[0].eval(),
                    2 => operand[0].eval() - operand[1].eval(),
                    _ => unreachable!(),
                },
                '*' => operand[0].eval() * operand[1].eval(),
                '/' => operand[0].eval() / operand[1].eval(),
                '%' => operand[0].eval() / 100f64,
                '^' => operand[0].eval().powf(operand[1].eval()),
                _ => unreachable!(),
            },
        }
    }

    fn from_lexer(lex: &mut lexer::Lexer, min_bp: u8) -> Result<Self, Error> {
        let mut lhs = match lex.next() {
            Some(lexer::Token::Atom(i)) => Expr::Atom(i),
            Some(lexer::Token::Op('(')) => {
                let lhs = Self::from_lexer(lex, 0)?;
                if lex.next() != Some(lexer::Token::Op(')')) {
                    return Err(Error::UnclosedParen);
                }
                lhs
            }
            Some(lexer::Token::Op(op)) => {
                if let Some(((), r_bp)) = Self::prefix_binding_power(op) {
                    let rhs = Self::from_lexer(lex, r_bp)?;
                    Expr::Cons(op, vec![rhs])
                } else {
                    return Err(Error::UnexpectedToken(lexer::Token::Op(op)));
                }
            }
            None => return Err(Error::EmptyExpr),
        };

        loop {
            let op = match lex.peek() {
                None => break,
                Some(lexer::Token::Op(op)) => *op,
                Some(t) => return Err(Error::UnexpectedToken(*t)),
            };

            if let Some((l_bp, ())) = Self::postfix_binding_power(op) {
                if l_bp < min_bp {
                    break;
                }

                lex.next();

                lhs = Expr::Cons(op, vec![lhs]);
                continue;
            }

            if let Some((l_bp, r_bp)) = Self::infix_binding_power(op) {
                if l_bp < min_bp {
                    break;
                }

                lex.next();
                let rhs = Self::from_lexer(lex, r_bp)?;

                lhs = Expr::Cons(op, vec![lhs, rhs]);
                continue;
            }

            break;
        }

        Ok(lhs)
    }
    fn prefix_binding_power(op: char) -> Option<((), u8)> {
        match op {
            '+' | '-' => Some(((), 5)),
            _ => None,
        }
    }

    fn infix_binding_power(op: char) -> Option<(u8, u8)> {
        match op {
            '+' | '-' => Some((1, 2)),
            '*' | '/' => Some((3, 4)),
            '^' => Some((8, 7)),
            _ => None,
        }
    }

    fn postfix_binding_power(op: char) -> Option<(u8, ())> {
        match op {
            '%' => Some((9, ())),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn valid_expr() {
        let s = Expr::new("1").unwrap();
        assert_eq!(s.to_string(), "1");

        let s = Expr::new("1.0 + 2 * 3").unwrap();
        assert_eq!(s.to_string(), "(+ 1 (* 2 3))");

        let s = Expr::new("1.0 + .2 * 3 / 4 - .5").unwrap();
        assert_eq!(s.to_string(), "(- (+ 1 (/ (* 0.2 3) 4)) 0.5)");
    }

    #[test]
    fn eval_expr() {
        let s = Expr::new("1").unwrap();
        assert_eq!(s.eval(), 1f64);

        let s = Expr::new("1.0 + 2 * 3").unwrap();
        assert_eq!(s.eval(), 1.0f64 + 2f64 * 3f64);

        let s = Expr::new("1.0 + .2 * 3 / 4 - .5").unwrap();
        assert_eq!(s.eval(), 1.0f64 + 0.2f64 * 3f64 / 4f64 - 0.5f64);

        let s = Expr::new("(-1 + 3) ^ 200%").unwrap();
        assert_eq!(s.eval(), (-1f64 + 3f64).powf(200f64 / 100f64));
    }
}
